import path from 'path';
import { readdir } from 'fs/promises';
import { FlowInput, FlowStartOptions, Workflow } from './workflow';
import { FlowPlayer, OrganPlayer, RolePlayer, UserPlayer } from './player';
import { BeginNode } from './node';
import { asyncMap } from './utils';
import { FlowInstance, FlowInstanceInfo } from './instance';
import { customAlphabet } from './nanoid';

const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const nanoid = customAlphabet(alphabet);

interface FlowInfo {
	id: string;
	name: string;
	versions: Record<string, Workflow>;
	versionLoaders: Record<string, (flow: Workflow) => void>;
	current: Workflow;
}

interface FlowLoader {
	id?: string;
	name: string;
	versions: Record<string, (flow: Workflow) => void>;
	current: (flow: Workflow) => void;
}

export interface TodoBaseInfo {
	flowId: string;
	flowVersion: string;
	instanceId: string;
	nodeId: string;
	nodeAppearId: number;
	players: UserPlayer[];
}

export interface TodoInfo extends TodoBaseInfo {
	flowName: string;
	nodeName: string;
	previousNodeId: string;
	previousNodeName: string;
	previousOpinion?: string;
	createTime: Date;
}

export type TodoRemove = Omit<TodoBaseInfo, 'players'> & { players?: UserPlayer[] };
export type TodoSummary = Omit<TodoInfo, 'players'>;

export interface WorkflowHandler {
	newInstanceId?(): Promise<string>;
	saveInstance(instance: FlowInstanceInfo): Promise<void>;
	deleteInstance(instance: FlowInstanceInfo): Promise<void>;
	loadInstance(flowId: string, flowVersion: string, instanceId: string): Promise<FlowInstanceInfo>;
	isValidUser(user: UserPlayer, players: (UserPlayer | RolePlayer | OrganPlayer)[]): Promise<boolean>;
	toUsers(players: (UserPlayer | RolePlayer | OrganPlayer)[]): Promise<UserPlayer[]>;
	getTodoList(player: UserPlayer): Promise<TodoSummary[]>;
	addTodo(todoInfo: TodoInfo): Promise<void>;
	removeTodo(todoInfo: TodoRemove): Promise<void>;
}

export class WorkflowManager {
	private flowInfos: Record<string, FlowInfo> = {};
	private ready?: Promise<void[]>;

	private constructor(private handler: WorkflowHandler) {}

	private async loadByPath(targetDir: string) {
		this.ready ? await this.ready : null;
		this.ready = readdir(targetDir).then((files) => {
			return asyncMap(
				files.filter((f) => f.endsWith('.js')),
				async (f) => {
					const id = path.basename(f, '.js');
					const flowLoader: FlowLoader = (await import(`${targetDir}/${id}`)).default;
					const flow = new Workflow(flowLoader.id ?? id, flowLoader.name, flowLoader.current.name);
					flowLoader.current(flow);
					flow.validate();
					this.flowInfos[id] = {
						id: flowLoader.id ?? id,
						name: flowLoader.name,
						versionLoaders: flowLoader.versions,
						versions: {},
						current: flow,
					};
				}
			);
		});
	}

	private async load(flow: Record<string, FlowInfo>) {
		this.ready ? await this.ready : null;
		Object.values(flow).forEach((f) => f.current.validate());
		this.flowInfos = flow;
		this.ready = undefined;
	}

	async getWorkflow(flowId: string, version?: string): Promise<Workflow> {
		this.ready ? await this.ready : null;
		const flowInfo = this.flowInfos[flowId];
		if (!flowInfo) {
			throw new Error(`Workflow [${flowId}] not found!`);
		}
		if (version) {
			let flow = flowInfo.versions[version];
			if (!flow) {
				const loader = flowInfo.versionLoaders[version];
				if (loader) {
					flow = new Workflow(flowId, flowInfo.name, version);
					loader(flow);
					flowInfo.versions[version] = flow;
				} else {
					throw new Error(`Workflow [${flowId}] version [${version}] not found!`);
				}
			}
			return flow;
		} else {
			return flowInfo.current;
		}
	}

	async getStartableWorkflows(player: UserPlayer): Promise<Workflow[]> {
		this.ready ? await this.ready : null;
		const flows = Object.values(this.flowInfos).map((info) => info.current);
		return (
			await asyncMap(flows, async (flow) => {
				const node = flow.nodes.find((n): n is BeginNode => n.type === 'begin');
				if (!node) {
					return null;
				}
				const result = await asyncMap(node.players, (p) => {
					if (FlowPlayer.isCustomPlayer(p)) {
						return p.isValidPlayer(player);
					} else {
						return this.handler.isValidUser(player, [p as UserPlayer | RolePlayer | OrganPlayer]);
					}
				});
				if (result.some((v) => v)) {
					return flow;
				} else {
					return null;
				}
			})
		).filter((v): v is Workflow => !!v);
	}

	async start(flowId: string, input: FlowStartOptions): Promise<FlowInstance> {
		const flow = await this.getWorkflow(flowId);
		const id = await this.newInstanceId();
		const instance = await flow.newInstance(this, id, input);
		await this.saveInstance(instance);
		return instance;
	}

	async go(instance: FlowInstance, input: FlowInput): Promise<void> {
		const flow = await this.getWorkflow(instance.flowId, instance.flowVersion);
		await flow.go(this, instance, input);
		await this.saveInstance(instance);
	}

	async saveInstance(instance: FlowInstance): Promise<void> {
		this.ready ? await this.ready : null;
		const info: FlowInstanceInfo = { ...instance };
		delete (info as unknown as Record<string, unknown>).manager;
		await this.handler.saveInstance(info);
	}

	async deleteInstance(instance: FlowInstance): Promise<void> {
		this.ready ? await this.ready : null;
		const info: FlowInstanceInfo = { ...instance };
		delete (info as unknown as Record<string, unknown>).manager;
		await this.handler.deleteInstance(info);
	}

	async loadInstance(flowId: string, flowVersion: string, instanceId: string): Promise<FlowInstance> {
		this.ready ? await this.ready : null;
		const info = await this.handler.loadInstance(flowId, flowVersion, instanceId);
		return info;
	}

	async newInstanceId(): Promise<string> {
		if (typeof this.handler.newInstanceId === 'function') {
			return this.handler.newInstanceId();
		} else {
			return nanoid(10);
		}
	}

	toUsers(players: (UserPlayer | RolePlayer | OrganPlayer)[]): Promise<UserPlayer[]> {
		return this.handler.toUsers(players);
	}

	async addTodo(todoInfo: TodoInfo): Promise<void> {
		this.ready ? await this.ready : null;
		return this.handler.addTodo(todoInfo);
	}

	async removeTodo(todoInfo: TodoRemove): Promise<void> {
		this.ready ? await this.ready : null;
		return this.handler.removeTodo(todoInfo);
	}

	async getTodoList(player: UserPlayer): Promise<TodoSummary[]> {
		this.ready ? await this.ready : null;
		return this.handler.getTodoList(player);
	}

	static loadByPath(path: string, handler: WorkflowHandler): WorkflowManager {
		const manager = new WorkflowManager(handler);
		manager.loadByPath(path);
		return manager;
	}

	static load(flow: Record<string, FlowInfo>, handler: WorkflowHandler): WorkflowManager {
		const manager = new WorkflowManager(handler);
		manager.load(flow);
		return manager;
	}
}
